#include "Ext2Filesystem.h"
#include <errno.h>
#include <jimix/File.h>
#include <stdio.h>
#include <jimix/Directory.h>
#include <jimix/ResourceManager.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

Ext2Filesystem::Ext2Filesystem()
{
  resource = createResource((Callable*)this);
  addFunction(resource, (Resource::Call4)&Ext2Filesystem::read, FILE_READ);
  addFunction(resource, (Resource::Call2)&Ext2Filesystem::populateDirectory, DIR_POPULATE);
  addFunction(resource, (Resource::Call1)&Ext2Filesystem::getSize, FILE_LENGTH);
  
  filesystem = NULL;
  fd = NULL;
  groupDescriptors = NULL;
}


Ext2Filesystem::~Ext2Filesystem()
{
  if (fd)
    fclose(fd);
  delete [] groupDescriptors;
}

int Ext2Filesystem::mount(const char *device, const char *path)
{
  
  // Check our target path names a directory.
  u32int resourceManager = requestResource("ResourceManager");
  
  u32int root = requestResource("Filesystem");
  u32int target = callResource(root, Filesystem_IF::traverse, path);
  
  if (!target || callResource(target, File_IF::identify) != eFileType_Directory)
    return ENOTDIR;
  
  // Check our device is valid.
  fd = fopen(device, "wb+");
  if (!fd)
    return ENOENT;
  
  char tmp2[1024];
  // Get the superblock.
  fseek(fd, 1024, SEEK_SET);
  fread((u8int*)&superblock, 1, sizeof(superblock_t), fd);
  
  if (superblock.s_magic != 0xEF53)
  {
    fclose(fd);
    fd = NULL;
    FILE *console = fopen("/dev/console", "w");
    fprintf(console, "[EXT2]\tSuperblock not found on device %s.\n", device);
    fclose(console);
    return EINVAL;
  }
  // Block size
  u32int blockSize = 1024 << superblock.s_log_block_size;
  
  // Load the group descriptor table.
  u8int *tmp = new u8int[blockSize];
  fseek(fd, blockSize*2, SEEK_SET);
  fread(tmp, 1, blockSize, fd);
  groupDescriptors = (group_desc_t*)tmp;
  
  filesystem = callResource(resourceManager, ResourceManager_IF::mkfs);
  
  root = callResource(filesystem, Filesystem_IF::mknod, eFileType_Directory, NULL, resource, EXT2_ROOT_INO);
  
  callResource(filesystem, Filesystem_IF::setRoot, root);
  
  callResource(target, Directory_IF::mount, filesystem);
  
  FILE *console = fopen("/dev/console", "w");
  fprintf(console, "[EXT2]\tMounted %s at %s.\n", device, path);
  fclose(console);
  
}

inode_t globInode;
int globInodeNo = -1;
inode_t Ext2Filesystem::getInode(u32int inode)
{
  inode--;
  
  if (inode == globInodeNo)
    return globInode;
  
  // Block size
  u32int blockSize = 1024 << superblock.s_log_block_size;
  
  u32int index = inode % superblock.s_inodes_per_group;
  u32int group = inode / superblock.s_inodes_per_group;
  
  // TODO:: add LOTS OF caching here. Feck me it's slow as it is!
  char *buffer = new char[1024];
  inode_t *inode_table = (inode_t*)buffer;//new inode_t[superblock.s_inodes_per_group];
  u32int inodeTableBlock = groupDescriptors[group].bg_inode_table;
  
  fseek(fd, inodeTableBlock*blockSize, SEEK_SET);
  fread((u8int*)inode_table, 1, blockSize, fd);
  inode_t idx = inode_table[index];
  delete [] buffer;
  
  globInodeNo = inode;
  globInode = idx;
  return idx;
  
}

// void Ext2Filesystem::readInode(inode_t inode, u8int *buffer, u64int offset, u64int size)
// {
//   s32int szLeft = (size==0)?(inode.i_size-offset):size;
//   
//   u64int start = offset;
//   u32int blockSize = 1024 << superblock.s_log_block_size;
//   
//   start /= blockSize;
// //   printf("sz %d.\n", szLeft);
//   int j = 0;
//   for (int i = start; i < 12; i++,j++)
//   {
//     u32int sz = (szLeft < blockSize)?szLeft:blockSize;
//     u8int *bin = new u8int[blockSize];
//     readBlock(inode.i_block[i], bin, sz);
//     memcpy(&buffer[i*blockSize], bin, (szLeft<blockSize)?szLeft:blockSize);
//     delete [] bin;
//     szLeft -= blockSize;
// //     printf ("szleft=%d\n", szLeft);
//     if (szLeft <= 0)
//       return; 
//   }
//   // TODO:: implement indirect addressing.
//   FILE *console = fopen("/dev/console", "w");
//   fprintf(console, "[EXT2]\tIndirect addressing not implemented!\n");
//   fclose(console);
// }

void Ext2Filesystem::getIndirectBlockAddresses(u32int blockn, u32int *blocks, u32int start, u32int number)
{
//   printf ("Get indirect block address! : start = %d, length = %d\n",start,number);
  u32int blockSize = 1024 << superblock.s_log_block_size;
  u32int inodesPerBlock = blockSize/sizeof(u32int);
  u32int *buf = new u32int[inodesPerBlock];
  readBlock(blockn, (u8int*)buf);

  memcpy(blocks, &buf[start], number*sizeof(u32int));
  delete [] buf;
}

void Ext2Filesystem::getBiindirectBlockAddresses(u32int blockn, u32int *blocks, u32int start, u32int number)
{
//   printf ("Get Biindirect block address! : start = %d, length = %d\n", start, number);
  u32int blockSize = 1024 << superblock.s_log_block_size;
  u32int inodesPerBlock = blockSize/sizeof(u32int);
  u32int *buf = new u32int[inodesPerBlock];
  readBlock(blockn, (u8int*)buf);
  
  u32int startBlock = start/inodesPerBlock;
  u32int startOffset = start%inodesPerBlock;
  
  u32int endBlock = (start+number)/inodesPerBlock;
  u32int endOffset = (start+number)%inodesPerBlock;
  
  int sz = inodesPerBlock-startOffset;
  u32int nSoFar = 0;
    
  for(int block = startBlock; block <= endBlock; block++)
  {
    if (block == endBlock && endOffset == 0)
      break;
    else if (block == endBlock)
    {
      sz = endOffset-startOffset;
    }
    getIndirectBlockAddresses(buf[block], &blocks[nSoFar], startOffset, sz);
    nSoFar += sz;
    sz = inodesPerBlock;
    startOffset = 0;
  }
  delete [] buf;
}

void Ext2Filesystem::readInode(inode_t inode, u8int *buffer, u32int offset, u32int size)
{
  size = (size==0) ? (inode.i_size-offset) : size;
  u32int blockSize = 1024 << superblock.s_log_block_size;
  
  u32int startBlock = offset/blockSize;
  u32int startOffset = offset%blockSize;
  
  u32int endBlock = (offset+size)/blockSize;
  u32int endOffset = (offset+size)%blockSize;
  
  u32int bufOffset = 0;
  u32int nBlocks = endBlock - startBlock +1;
  u32int *blocks = new u32int[nBlocks+32];
//   printf("NBLOCKS %d\n", nBlocks);
  u32int inodesPerBlock = blockSize/sizeof(u32int);
  u32int inodesPerBlock2 = inodesPerBlock*inodesPerBlock;
  u32int nBlocksDone = 0;
//   printf("Accessing offset %x", (u32int)offset);
//       printf(", size %d\n", (u32int)size);
//   printf("Reading %d blocks, starting at %d ending at %d.\n", nBlocks, startBlock, endBlock);
  if (startBlock < 12)
  {
    nBlocksDone = (endBlock<12) ? nBlocks : (12-startBlock);
//     printf("Blocks 0 to %d local.\n", nBlocksDone);
    memcpy(blocks, &inode.i_block[startBlock], sizeof(u32int)*nBlocksDone);
  }
  if ( (startBlock < inodesPerBlock+12) &&
       (endBlock > 12))
  {
    // Get indirect block addresses.
    u32int bStart = (startBlock < 12) ? 0 : startBlock-12;
    u32int bEnd   = (endBlock < inodesPerBlock+12) ? endBlock-12+1 : inodesPerBlock;
//     printf("Blocks %d to %d done indirectly.\n", nBlocksDone, (nBlocksDone+bEnd-bStart));
    getIndirectBlockAddresses(inode.i_block[12], &blocks[nBlocksDone], bStart, bEnd-bStart);
    nBlocksDone += bEnd-bStart;
  }
  if ( (startBlock < inodesPerBlock2+12) &&
       (endBlock > inodesPerBlock+12) )
  {
    // Get bi-indirect block addresses.
    u32int bStart = (startBlock < inodesPerBlock+12) ? 0 : startBlock-(inodesPerBlock+12);
    u32int bEnd   = (endBlock < inodesPerBlock2+12) ? endBlock-(inodesPerBlock+12)+1 : inodesPerBlock2;
//     printf("Blocks %d to %d done biindirectly.\n", nBlocksDone, (nBlocksDone+bEnd-bStart));
    getBiindirectBlockAddresses(inode.i_block[13], &blocks[nBlocksDone], bStart, bEnd-bStart);
    nBlocksDone += bEnd-bStart;
  }
  if ( (startBlock > inodesPerBlock*inodesPerBlock+12) ||
       (endBlock > inodesPerBlock*inodesPerBlock+12))
  {
    printf ("ERR!ERR!ERR!ERR!ERR!ERR!ERR!ERR!\n");
    return;
  }

  for (int block = 0; block < nBlocks; block++)
  {
//      printf ("block[%d] = %x\n", block, blocks[block]);
    u32int sz = blockSize;

    if (block == nBlocks-1 && endOffset == 0)
      break;
    else if (block == nBlocks-1)
    {
      sz = endOffset;
    }

    if (startOffset != 0)
    {
      sz = sz-startOffset;
      u8int *buf = new u8int[blockSize];
      if (blocks[block] > 0x6000 || blocks[block] < 0x5e02)
      readBlock(blocks[block], buf);
//       printf("st: %d, en: %d\n", startOffset, endOffset);
//       printf("memcpying %d sz %d\n", startOffset, sz);
      memcpy(&buffer[bufOffset], &buf[startOffset], sz);
      delete [] buf;
//       printf("...done\n");
      startOffset = 0;
    }
    else
    {
//       printf("reading block to %d", bufOffset);
//       printf("sz %d\n", sz);
      if (blocks[block] > 0x6000 || blocks[block] < 0x5e02)
      readBlock(blocks[block], &buffer[bufOffset], sz);
//       printf("...done\n");
    }
    bufOffset += sz;
  }
  delete [] blocks;
}

void Ext2Filesystem::populateDirectory(u32int inode, u32int directoryResource)
{

  // Firstly get the inode for the directory.
  inode_t node = getInode(inode);
  
  // Is the directory in hashed mode?
  if (node.i_flags & 0x10000)
  {
    // TODO:: implement hashed mode directories.
    FILE *console = fopen("/dev/console", "w");
    fprintf(console, "[EXT2]\tHashed mode directories not implemented yet!");
    fclose(console);
    return;
  }

  char *buffer = new char[node.i_size];
  dir_t *directory;
  
  readInode(node, (u8int*)buffer);
  
  // Initially traverse the directory to find the number of entries.
  int nEntries = 0;
  directory = (dir_t*)buffer;
  while (*(char*)directory != 0)
  {
    nEntries++;
    directory = (dir_t*) ((u32int)directory + directory->reclen);
  }
  
  // Allocate nFiles files.
  u32int *files = new u32int[nEntries];
  
  // Traverse again!
  directory = (dir_t*)buffer;
  for (int i = 0; i < nEntries; i++)
  {
    // Create a copy of the name on the heap
    char *name = new char[directory->namelen+1];
    strncpy(name, directory->name, directory->namelen);
    name[directory->namelen] = '\0';
    
    u32int impl = directory->inode;
    u32int filetype = 0;
    
    switch (directory->file_type)
    {
    case EXT2_FILE:
      filetype = eFileType_RegularFile;
      break;
    case EXT2_DIRECTORY:
      filetype = eFileType_Directory;
      break;
    case EXT2_CHAR_DEV:
      filetype = eFileType_CharDevice;
      break;
    case EXT2_BLOCK_DEV:
      filetype = eFileType_BlockDevice;
      break;
    default:
      filetype = eFileType_RegularFile;
    }
    
    callResource(directoryResource, Directory_IF::mknod, filetype,
                 name, resource, impl);
    directory = (dir_t*) ((u32int)directory + directory->reclen);
    delete [] name;
  }

  delete [] buffer;
  delete [] files;
}

void Ext2Filesystem::readBlock(u32int block, u8int *buffer, u32int limitSize)
{
  u32int sblock = 1024 << superblock.s_log_block_size;
  fseek(fd, block*sblock, SEEK_SET);
  if (limitSize && limitSize < sblock)
    sblock = limitSize;
  fread(buffer, 1, sblock, fd);
}

u32int Ext2Filesystem::getSize(u32int inode)
{
  inode_t node = getInode(inode);
  u64int toRet = (u64int)node.i_dir_acl << 32;
  toRet |= node.i_size;
  return toRet;
}

u32int Ext2Filesystem::read(u32int inode, u32int offset, u32int size, u8int *buffer)
{
//   printf ("doing read of %d bytes.\n", size);
  if (size == 0) return 0;
  inode_t node = getInode(inode);
  readInode(node, buffer, offset, size);
//   printf ("ending.\n");
  return size;
}
